package org.ykh.service.config;


import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.converter.ResolvedSchema;

import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.ykh.service.result.Result;
import java.lang.reflect.ParameterizedType;

import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * @author ykh
 */
@Configuration
public class SwaggerCustomize {

    private Map<String, Schema> respSchemas = new ConcurrentHashMap<>();

    @Bean
    public OpenApiCustomiser addGenericSchemaToOpenApi() {
        return openApi -> {
            Map<String, Schema> schemas = openApi.getComponents().getSchemas();

            Set<String> shouldRemoveRespSchemas = schemas.entrySet()
                    .stream()
                    .filter(kv -> {
                        if (!kv.getKey().startsWith(Result.class.getSimpleName())) {
                            return false;
                        }
                        Map fieldsProperties = kv.getValue().getProperties();
                        Field[] respFields = Result.class.getDeclaredFields();
                        if (fieldsProperties.size() != respFields.length) {
                            return false;
                        }

                        Set<String> respFieldSet = Arrays.stream(respFields)
                                .map(Field::getName)
                                .collect(Collectors.toSet());

                        if (Sets.intersection(fieldsProperties.keySet(), respFieldSet).size() != respFields.length) {
                            return false;
                        }
                        return true;
                    })
                    .map(Map.Entry::getKey)
                    .collect(Collectors.toSet());

            shouldRemoveRespSchemas.forEach(schemas::remove);
            schemas.putAll(respSchemas);
        };
    }

    @Bean
    public OperationCustomizer customize() {
        // 1. collect ResultXxx
        // 2. replace $ref ResultXxx to Result<Xxx>

        return (operation, method) -> {
            final Logger logger = LoggerFactory.getLogger(this.getClass());
            ApiResponses responses = operation.getResponses();

            Class<?> returnType = method.getMethod().getReturnType();
            if (
                    returnType.equals(Result.class)
            ) {
                ResolvedSchema baseRespSchema = ModelConverters.getInstance()
                        .resolveAsResolvedSchema(new AnnotatedType(Result.class));

                Map<String, Schema> fieldsSchema = Maps.newLinkedHashMap();
                fieldsSchema.putAll(baseRespSchema.schema.getProperties());

                Method methodMethod = method.getMethod();
                Type type = methodMethod.getGenericReturnType();

                Class actualTypeArgument = null;
                if (type instanceof ParameterizedType) {//Result泛型类是否声明了泛型。
                    ParameterizedType genericReturnType = (ParameterizedType) type;
                    Type typeArgument =
                            genericReturnType.getActualTypeArguments()[0];
                    String typeName = typeArgument.getTypeName();
                    if ("?".equals(typeName) || Object.class.getName().equals(typeName)){//是否声明了具体类型
                        Schema schema = new ObjectSchema().type("object")
                                .properties(fieldsSchema)
                                .name("Result");
                        respSchemas.put("Result", schema);
                        logger.debug("检测到不明确的泛型类型：{}",typeName);
                        return operation;
                    }else {
                        actualTypeArgument = (Class) typeArgument;
                    }
                }


                ResolvedSchema resolvedSchema = ModelConverters.getInstance()
                        .resolveAsResolvedSchema(new AnnotatedType(actualTypeArgument));
                String respSchemaName;
                if (resolvedSchema.schema != null) {
                    // override data field schema
                    if (resolvedSchema.referencedSchemas.isEmpty()) {
                        fieldsSchema.put("data", resolvedSchema.schema);
                    } else {
                        fieldsSchema.put("data", new ObjectSchema().$ref(actualTypeArgument.getSimpleName()));
                    }

                    respSchemas.putAll(resolvedSchema.referencedSchemas);

                    respSchemaName = "Result<" + actualTypeArgument.getSimpleName() + ">";
                } else {
                    // override data field schema
                    Schema originDataSchema = fieldsSchema.get("data");
                    fieldsSchema.put("data", new MapSchema()
                            .description(originDataSchema.getDescription())
                            .nullable(originDataSchema.getNullable()));

                    respSchemaName = "Result<Map>";
                }

                Schema schema = new ObjectSchema().type("object")
                        .properties(fieldsSchema)
                        .name(respSchemaName);

                // // replace ref '#/components/schemas/ResultXxx' to '#/components/schemas/Result<Xxx>'
                for (ApiResponse apiResponse : responses.values()) {
                    for (MediaType mediaType : apiResponse.getContent().values()) {
                        Schema originApiResponseSchema = mediaType.getSchema();
                        if (originApiResponseSchema.get$ref() != null
                                && originApiResponseSchema.get$ref().startsWith("#/components/schemas/Result")) {
                            originApiResponseSchema.$ref(schema.getName());
                        }
                    }
                }

                respSchemas.put(respSchemaName, schema);
            }
            return operation;
        };
    }

}
